/*
 * To update this template, choose Tools | Templates
 * and open the template in the editor.
 */
package be.hogent.iii.project.newconnectie;

/**
 *
 * @author Dieter Van der Stock
 */
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.*;

public class Client implements Runnable {
    // De combinatie van adres en poortnummer waar we naar luisteren
    private InetAddress hostAdres;
    private int poort;

    // De selector waar het allemaal om draait
    private Selector selector;
    
    //weergeven of we al een verbinding hebben
    private boolean verbonden;

    // De buffer waar we onze gelezen data in opslagen
    private ByteBuffer leesBuffer = ByteBuffer.allocate(8192);

    // Een lijst van veranderingen die we moeten doorvoeren
    private List updateLijst = new LinkedList();

    // Houdt bij welke socket dat welke data moet krijgen (hier natuurlijk meestal 1 socket)
    private Map teSturenData = new HashMap();
    
    // Houdt bij welke socket dat bij welke handler hoort (beetje algemeen voor hier, maar kan nog van pas komen)
    private Map rspHandlers = Collections.synchronizedMap(new HashMap());
    
    private SocketChannel socket;

    /**
     * Zal onze client voorzien van de nodige info, namelijk het adres en poortnummer van de server
     * @param hostAddress Het adres waar de server zich moet bevinden
     * @param port De poort waar de server zou moeten luisteren
     * @throws java.io.IOException
     */
    public Client(InetAddress hostAddress, int port) throws IOException {
        this.hostAdres = hostAddress;
        this.poort = port;
        this.selector = this.initSelector();
        this.verbonden = false;
        this.socket = this.maakVerbinding();
    }

    /**
     * Zal data klaarzetten om te verzenden zodra het mogelijk is.
     * 
     * @param data
     * @param handler
     * @throws java.io.IOException
     */
    public void queueTeVerzendenData(byte[] data, Handler handler) throws IOException {      

        // We houden bij welke handler bij onze socket hoort (niet perse nodig denk ik)
        //XML: ik veronderstel dat dat de delegeerklasse wordt van de XML Parser
        this.rspHandlers.put(socket, handler);
        

        // We steken de data die we willen schrijven naar onze socket in een queue
        synchronized (this.teSturenData) {
            List queue = (List) this.teSturenData.get(socket);
            if (queue == null) { //als er nog geen entry voor deze socket bestaat
                queue = new ArrayList();
                this.teSturenData.put(socket, queue);
            }
            queue.add(ByteBuffer.wrap(data)); // we steken de nieuwe data die we willen schrijven bij in de queue
        }

        //we hebben nu interesse om te schrijven
        synchronized (this.updateLijst) {
            this.updateLijst.add(new updateAanvraag(socket, updateAanvraag.CHANGEOPS, SelectionKey.OP_WRITE));
            //System.out.println("in queueteschrijven, ops naar write veranderd");
        }
        // Uiteindelijk maken we onze selector wakker om alles door te voeren
        this.selector.wakeup();
    }

    /**
     * De run() methode van onze client, het hart van heel het proces.
     */
    public void run() {
        //System.out.println("DEBUGIn run");
        while (true) {
            try {
                // We voeren de nodige updates door
                synchronized (this.updateLijst) {
                    Iterator updates = this.updateLijst.iterator();
                    while (updates.hasNext()) {
                        updateAanvraag update = (updateAanvraag) updates.next();
                        switch (update.type) { //zien welk type update we hebben
                            case updateAanvraag.CHANGEOPS:
                                SelectionKey key = update.socket.keyFor(this.selector);
                                key.interestOps(update.ops);
                                break;
                            case updateAanvraag.REGISTER:
                                update.socket.register(this.selector, update.ops);
                                break;
                        }
                    }
                    this.updateLijst.clear();
                }

                // Wait for an event one of the registered channels
                this.selector.select();

                // Iterate over the set of keys for which events are available
                Iterator selectedKeys = this.selector.selectedKeys().iterator();
                while (selectedKeys.hasNext()) {
                    SelectionKey key = (SelectionKey) selectedKeys.next();
                    selectedKeys.remove();

                    //als de key om één of andere reden niet deugd, skippen we deze iteratie
                    if (!key.isValid()) {
                        continue;
                    }

                    // Zien welk soort event we te pakken hebben in onze key en de juiste procedure oproepen
                    if (key.isConnectable()) {
                        this.vervolledigVerbinding(key);
                    } 
                    else if (key.isReadable()) {
                        //System.out.println("DEBUGIn run iets leesbaar!");
                        this.leesData(key);
                    } 
                    else if (key.isWritable()) {
                        this.verzendData(key);
                    }
                }
            } catch (Exception e) {
                System.out.println("Er is een fout opgetreden in run(): " + e.getMessage());
                e.printStackTrace();
            }
        }
    }

    /**
     * Zal data lezen van een connectie die daarvoor 'vraagt'
     * 
     * @param key De verbinding die data heeft klaarstaan
     * @throws java.io.IOException
     */
    private void leesData(SelectionKey key) throws IOException {
        SocketChannel socketKanaal = (SocketChannel) key.channel();
        
        //System.out.println("DEBUGIn leesdata");

        // Onze buffer leegmaken voor we iets nieuw erin steken
        this.leesBuffer.clear();

        // we proberen data te lezen van ons kanaal
        int aantalBytesGelezen;
        try {
            aantalBytesGelezen = socketKanaal.read(this.leesBuffer);
            //System.out.println("DEBUGIn leesdata in den try net iets gelezen");
        } catch (IOException e) {
            System.out.println("Bij de server liep er plots iets mis. We cancellen de aanvraag en sluiten het kanaal naar " + socketKanaal.socket().getRemoteSocketAddress());
            key.cancel();
            socketKanaal.close();
            return;
        }

        if (aantalBytesGelezen == -1) {
            System.out.println("De server heeft zijn verbinding afgesloten. We cancellen de aanvraag en sluiten het kanaal naar " + socketKanaal.socket().getRemoteSocketAddress());
            key.channel().close();
            key.cancel();
            return;
        }

        // Behandel het bericht
        //System.out.println("DEBUGIn leesdata wordt gestuurd naar behandelbericht");
        this.behandelBericht(socketKanaal, this.leesBuffer.array(), aantalBytesGelezen);
    }

    /**
     * Zal een bericht van de server
     * 
     * @param socketKanaal Waar het vandaan komt.
     * @param data Het bericht dat we binnen kregen
     * @param aantalGelezenBytes
     * @throws java.io.IOException
     */
    private void behandelBericht(SocketChannel socketKanaal, byte[] data, int aantalGelezenBytes) throws IOException {
        // We maken een mooie kopie van de data, deze geven we door aan de handler
        byte[] bericht = new byte[aantalGelezenBytes];
        System.arraycopy(data, 0, bericht, 0, aantalGelezenBytes);

        // We zoeken welke klasse dit bericht heeft aangevraagd
        //UPDATE: zoiets hebben we niet perse nodig.. Maar tis wel mooi natuurlijk
        Handler handler = (Handler) this.rspHandlers.get(socketKanaal);

        //System.out.println("DEBUGIn behandelbericht, doorgestuurd naar handler");
        // En geven het bericht aan de handlerklasse door
        handler.behandelBericht(bericht);
    }

    /**
     * Zal de queue van te verzenden berichten afwerken voor het kanaal dat zegt dat het er klaar voor is
     * @param key Het kanaal dat klaar is om data te versturen
     * @throws java.io.IOException
     */
    private void verzendData(SelectionKey key) throws IOException {
        SocketChannel socketKanaal = (SocketChannel) key.channel();
        
        //System.out.println("DEBUGIn verzenddata");

        synchronized (this.teSturenData) {
            List queue = (List) this.teSturenData.get(socketKanaal);

            // Schrijven tot er geen data meer is
            while (!queue.isEmpty()) {
                ByteBuffer buf = (ByteBuffer) queue.get(0);
                System.out.println("We sturen een bericht naar de server.");
                socketKanaal.write(buf);
                if (buf.remaining() > 0) {
                    // of tot de socket vol zit
                    break;
                }
                queue.remove(0);
            }

            if (queue.isEmpty()) {
                //We hebben alle data geschreven, dus gaan we terug wachten op het lezen van data
                key.interestOps(SelectionKey.OP_READ);
                //System.out.println("DEBUGIn verzenddata OPS veranderd");
            }
        }
    }

    /**
     * Hier vervolledigen we mooi de verbinding naar de server
     * @param key Het kanaal dat zegt dathet klaar is om de verbinding te vervolledigen
     * @throws java.io.IOException
     */
    private void vervolledigVerbinding(SelectionKey key) throws IOException {
        SocketChannel socketKanaal = (SocketChannel) key.channel();

        //We maken de connectie af, of handelen de exceptie af als dit mislukt
        try {
            socketKanaal.finishConnect();
            verbonden = true;
            //System.out.println("We zijn verbonden met de server op " + socketKanaal.socket().getRemoteSocketAddress());
        } catch (IOException e) {
            System.out.println("Er ging iets mis met het vervolledigen van onze verbinding met " + socketKanaal.socket().getRemoteSocketAddress());
            System.out.println("De verbindingsaanvraag wordt gecancelled.");
            System.out.println(e);
            key.cancel();
            return;
        }

        // Nu pas registeren we dat we willen schrijven
        //CHAT: als we nog niet direct willen schrijven, kunnen we dit best nu nog niet doen!! (als dat ooit gebeurt)
        //dan doen we dit later en schrijven we dan pas naar de server. Pas wel op of dit dan in updatelijst moet of niet
        //key.interestOps(SelectionKey.OP_WRITE); //mss moeten we dat verplaatsen naar queueTeVerzendenData?
    }

    /**
     * We maken een verbinding aan naar de server (het adres werd opgegeven in de constructor)
     * @return Het kanaal dat gemaakt is
     * @throws java.io.IOException
     */
    private SocketChannel maakVerbinding() throws IOException {
        // we maken een non-blocking socketkanaal
        SocketChannel socketKanaal = SocketChannel.open();
        socketKanaal.configureBlocking(false);

        // We starten de verbinding
        socketKanaal.connect(new InetSocketAddress(this.hostAdres, this.poort));

        //we zetten de channelregistratie in een queue, aangezien de aanroeper niet de selecting-thread is
        //we zetten onderandere een registratie voor connection-events, deze worden opgeworpen wanneer een kanaal
        //klaar is voor de vebrinding te vervolledigen
        synchronized (this.updateLijst) {
            this.updateLijst.add(new updateAanvraag(socketKanaal, updateAanvraag.REGISTER, SelectionKey.OP_CONNECT));
        }
        //we maken hierna nog niet de selectingthread wakker, want we willen eerst wat data klaarzetten enzo
        //daarvoor moeten we ook eerst voor verzendData registreren enzo, en anders kan het zijn dat de selectiethread 
        //de connectie vervolledigd alvorens dat er data is gequeue-ed en de OP_WRITE is gezet
        return socketKanaal;
    }

    /**
     * We initten onze selector, hetgene waar heel de Java NIO om draait
     * @return
     * @throws java.io.IOException
     */
    private Selector initSelector() throws IOException {
        // Maak een nieuwe selector aan
        return Selector.open();
    }


    
    /* Te doen:
     * - DONE (denk ik) zorgen dat er niet telkens een nieuwe entry in de socketlijst is als queueTeVerzenden data wordt
     * opgeroepen. De verbinding moeten we ergens anders steken 
     * - wachtOpBericht ergens wat mooier zetten, niet zo ertussen geflanst -> moet geintegreerd worden met de XML Parser
     * - DONE (denk ik) een contructor ipv een main methode zodat de bovenliggende lagen de connectie kunnen oproepen
     * */
}

